package controller;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import utils.MutableInteger;
import utils.PlayerSides;
import view.ChessGUI;
import view.GuiViewController;
import view.ViewController;
import model.Board;
import model.BoardManager;
import model.Model;
import model.pieces.King;
import model.pieces.Knight;
import model.pieces.Pawn;
import model.pieces.Piece;

/**
 * Class manages game's logic
 */
public class GameManager implements Controller {
	

	private static final String[] REPLACING_PIECES = {"Queen", "Rook", "Bishop", "Knight"}; // options for pieces to replace a Pawn that reached board end
	private static final int DEFAULT_REPLACING_PIECE = 0; // default option for piece to replace a Pawn that reached board end
	private static final int MAX_PIECES_PARTIAL_GAME = 8; // maximal pieces for each player in partial game
			
	private Model boardManager; // manages chess game board
	private ViewController view; // manages display
	
	private List<Player> players; // holds game's players
	private PlayerSides.Sides currPlayerSide; // indicates which player plays at current turn
	private boolean notifyToPlayers; // indicates if should notify data to players
	
	private PreTurnHandler preTurnHandler; // performs required task before each turn
	private MutableInteger turnCounter; // counts turns in game
	private KingThreat threatAnalyzer; // analyzes threats on a King
	private GameTerminator gameTerminator; // determines if should end game
	boolean keepPlaying; // indicates if should keep playing the game
		
	private Point killerCreature; // position of Creature that has killed a game piece	
	private List<Point> killedCreatures; // holds positions of killed Creatures at current turn

	/**
	 * Constructor- creates a new game manager
	 * @param displayManager- manages game display
	 */
	public GameManager(ChessGUI displayManager) {
		
		// set a board manager
		boardManager = new BoardManager();
		
		// set view controller
		view = new GuiViewController(displayManager);
		
		// set players list
		players = new ArrayList<Player>(2);
		
		// initialize turn counter
		turnCounter = new MutableInteger(0);
		
		// allocate a threat analyzer
		threatAnalyzer = new ThreatAnalyzer(boardManager, this);
		
		// initialize flag for keep playing
		keepPlaying = false;
		
		// initialize killer Creature position
		killerCreature = null;
		
		// allocate list to hold killed Creatures positions
		killedCreatures = new LinkedList<Point>();
	}
	
	/**
	 * Returns the game board
	 */
	@Override
	public Board getBoard() { return boardManager.getBoard(); }
	
	/**
	 * Sets system for an automatic game
	 */
	@Override
	public void setAutomaticGame() {
		
		// remove all Creatures from Board
		boardManager.removeCreatures();
		
		// initialize game display
		view.initializeDisplay();
		
		// set board
		boardManager.setBoardStandard();

		// set players
		players.clear();
		players.add(new ComputerPlayer(PlayerSides.Sides.White, boardManager.getFirstPlayerPieces(), boardManager.getBoard(), this));		
		players.add(new ComputerPlayer(PlayerSides.Sides.Black, boardManager.getSecondPlayerPieces(), boardManager.getBoard(), this));

		// no notifications in automatic game
		notifyToPlayers = false;
		
		// initialize turn counter
		turnCounter.set(0);
		
		// set pre-turn handler
		preTurnHandler = new PreTurnHandler() {
			
			@Override
			public void preTurn() {
				// increment turn counter
				turnCounter.inc();
				return;
			}
		};
		
		// set game terminator
		gameTerminator = new ComputerGameTerminator(threatAnalyzer, view, turnCounter);
		
		// display board
		view.displayBoard();
	}
	
	/**
	 * Sets system for a two players game
	 */
	@Override
	public void setTwoPlayersGame() {
		
		// remove all Creatures from Board
		boardManager.removeCreatures();
		
		// initialize game display
		view.initializeDisplay();
		
		// set board
		boardManager.setBoardStandard();
		
		// set players
		players.clear();		
		players.add(new HumanPlayer(PlayerSides.Sides.White, boardManager.getFirstPlayerPieces(), boardManager.getBoard(), view));		
		players.add(new HumanPlayer(PlayerSides.Sides.Black, boardManager.getSecondPlayerPieces(), boardManager.getBoard(), view));

		// display notifications
		notifyToPlayers = true;
		
		// set pre-turn handler
		preTurnHandler = new PreTurnHandler() {
			
			@Override
			public void preTurn() {
				// two players game requires no tasks before turns
				return;
			}
		};
		
		// set game terminator
		gameTerminator = new HumanGameTerminator(threatAnalyzer, view);
		
		// display board
		view.displayBoard();
	}
	
	/**
	 * Sets system for a single player game
	 */
	@Override
	public void setSinglePlayerGame() {
		
		// remove all Creatures from Board
		boardManager.removeCreatures();
		
		// initialize game display
		view.initializeDisplay();
		
		// set board
		boardManager.setBoardStandard();
		
		// set players
		players.clear();
		players.add(new HumanPlayer(PlayerSides.Sides.White, boardManager.getFirstPlayerPieces(), boardManager.getBoard(), view));		
		players.add(new ComputerPlayer(PlayerSides.Sides.Black, boardManager.getSecondPlayerPieces(), boardManager.getBoard(), this));

		// display notifications
		notifyToPlayers = true;
		
		// set pre-turn handler
		preTurnHandler = new PreTurnHandler() {
			
			@Override
			public void preTurn() {
				// two players game requires no tasks before turns
				return;
			}
		};
		
		// set game terminator
		gameTerminator = new HumanGameTerminator(threatAnalyzer, view);
		
		// display board
		view.displayBoard();
	}
	
	/**
	 * Sets the board for adding pieces according to custom layout
	 */
	@Override
	public void setPartialGameBoard() {
		
		// remove all Creatures from Board
		boardManager.removeCreatures();
		
		// initialize game display
		view.initializeDisplay();
		
		// clear board
		boardManager.clearBoard();
		
		// display board
		view.displayBoard();
		
		// allow user to add pieces
		view.setPiecesFromUser();
	}
	
	/**
	 * Sets system for a partial game (after board was set)
	 */
	@Override
	public void setPartialGame() {
		
		// set players
		players.clear();
		players.add(new HumanPlayer(PlayerSides.Sides.White, boardManager.getFirstPlayerPieces(), boardManager.getBoard(), view));		
		players.add(new HumanPlayer(PlayerSides.Sides.Black, boardManager.getSecondPlayerPieces(), boardManager.getBoard(), view));

		// display notifications
		notifyToPlayers = true;
		
		// set pre-turn handler
		preTurnHandler = new PreTurnHandler() {
			
			@Override
			public void preTurn() {
				// two players game requires no tasks before turns
				return;
			}
		};
		
		// set game terminator
		gameTerminator = new HumanGameTerminator(threatAnalyzer, view);
		
		// display board
		view.displayBoard();
	}
	
	/**
	 * Sets system for a special game
	 */
	@Override
	public void setSpecialGame() {
		
		// remove all Creatures from Board
		boardManager.removeCreatures();
		
		// initialize game display
		view.initializeDisplay();
		
		// set board
		boardManager.setBoardStandard();

		// set players
		players.clear();
		players.add(new ComputerPlayer(PlayerSides.Sides.White, boardManager.getFirstPlayerPieces(), boardManager.getBoard(), this));		
		players.add(new ComputerPlayer(PlayerSides.Sides.Black, boardManager.getSecondPlayerPieces(), boardManager.getBoard(), this));

		// no notifications in special game
		notifyToPlayers = false;
		
		// initialize turn counter
		turnCounter.set(0);
		
		// set pre-turn handler
		preTurnHandler = new SpecialGamePreTurn(this, turnCounter);
		
		// set game terminator
		gameTerminator = new ComputerGameTerminator(threatAnalyzer, view, turnCounter);
		
		// display board
		view.displayBoard();
	}

	/**
	 * Runs game
	 */
	@Override
	public void startGame() {
		
		keepPlaying = true;
		int currPlayerIndex = 0;

		List<Point> prevCollidedPositions = new LinkedList<Point>();
		
		// keep playing until game is over
		while (keepPlaying) {
			
			// pre-turn activity
			preTurnHandler.preTurn();
			
			// clean killed Creatures
			view.cleanKilledCreatures(prevCollidedPositions, killedCreatures);
			
			// get current player
			Player currPlayer = players.get(currPlayerIndex);
			
			// get current player's side
			currPlayerSide = currPlayer.getPlayerSide();

			// check force stop
			if (!keepPlaying) {
				break;
			}
			
			// choose a move
			Movement movement = currPlayer.chooseMove();

			// display Creatures
			view.refreshDisplayBeforeMove(boardManager.getCreaturePositions(), boardManager.getCollidedCreaturePositions());
						
			// handle the chosen move
			boolean isCheckAfterMove = handleMovement(currPlayer, movement);
			
			// get next player index
			currPlayerIndex = setNextPlayerIndex(currPlayerIndex);
			
			// refresh board display
			view.refreshDisplayAfterMove(movement, killerCreature);

			// update current collided positions
			prevCollidedPositions.clear();
			prevCollidedPositions.addAll(boardManager.getCollidedCreaturePositions());
			
			// initialize killer Creature position
			killerCreature = null;

			// handle a case of moving a Pawn
			boolean isCheckAfterReplacing = handlePawn(movement.getDestination(), currPlayer);
			
			// check if should end game
			if (isGameOver()) {
				// stop playing
				keepPlaying = false;
				continue;
			}
			
			// check if should notify for Check
			if (notifyToPlayers) {
				
				// check if game has reached Check
				if (isCheckAfterMove || isCheckAfterReplacing) {
					view.notifyCheck();
				}
			}
		}
	}

	/**
	 * Handles movement- if piece to be moved is killed by a Creature while moving, piece is removed and no move is done.
	 * Otherwise, makes move and checks if game is in Check state.
	 * @param currPlayer- current player side
	 * @param movement- current movement in game
	 * @return True if game is in Check after the move, False if not
	 */
	private boolean handleMovement(Player currPlayer, Movement movement) {
		
		// indicator for Check
		boolean isCheck = false;
		
		// handle Creatures			
		if (isKilledByCreature(movement)) {
			
			// remove piece
			boardManager.removePiece(movement.getSource(), boardManager.getPiece(movement.getSource().x, movement.getSource().y).getPlayer());
			
		} else {
			
			// make chosen move
			boardManager.movePiece(currPlayer.getPlayerSide(), movement.getSource(), movement.getDestination());
			
			// verify if Check
			if (threatAnalyzer.isCheck(PlayerSides.getOpponentSide(currPlayerSide))) {
				// notify that game has reached Check
				isCheck = true;
			}				
		}
		
		// return Check indicator
		return isCheck;
	}

	/**
	 * Checks if moved piece is a Pawn. If so, updates that the Pawn has already made its first move and checks if the Pawn
	 * has reached board end. If so, asks the player for a piece to replace the Pawn.
	 * If the Pawn was replaced, checks if the game is in Check state
	 * @param destination- current movement's destination position
	 * @param currPlayer- current player side
	 * @return True if game is in Check after replacing the Pawn, False if not
	 */
	private boolean handlePawn(Point destination, Player currPlayer) {
		
		// indicates if replacing the Pawn caused Check
		boolean isCheck = false;
		
		// check if moving a Pawn
		Piece movedPiece = boardManager.getPiece(destination.x, destination.y); 
		if (movedPiece instanceof Pawn) {
			
			// update that first move has already been made
			((Pawn)(movedPiece)).updateFirstMove();		
			
			// check if Pawn has reached board end
			if (boardManager.isPieceAtEnd(movedPiece, destination)) {
				
				// get player's choice for piece
				int choice = currPlayer.getReplacingPawnOption(REPLACING_PIECES, DEFAULT_REPLACING_PIECE); //view.replacePawn(REPLACING_PIECES, DEFAULT_REPLACING_PIECE);
				
				// check if choice is valid
				String pieceType = null;
				if (0 <= choice && choice < REPLACING_PIECES.length) {
					
					// choice is valid, follow user's choice
					pieceType = REPLACING_PIECES[choice];
					
				} else {
					
					// invalid choice, keep the Pawn at its position
					pieceType = "Pawn";
				}
				
				// remove Pawn
				boardManager.removePiece(destination, currPlayerSide);
				
				// replace with the specified piece
				boardManager.addPiece(destination, pieceType, currPlayerSide);
				
				// check if replacing the Pawn caused Check
				isCheck = threatAnalyzer.isCheck(PlayerSides.getOpponentSide(currPlayerSide));
			}
		}
		
		// return Check indicator
		return isCheck;
	}
	
	/**
	 * Returns next player index
	 * @param currIndex- current player index
	 * @return next player index
	 */
	private int setNextPlayerIndex(int currIndex) {
		
		// proceed cyclicly on player list
		return (currIndex + 1) % players.size();
	}
	
	/**
	 * Uses the game terminator to check if game should end
	 * @return True if game is over, False if not
	 */
	private boolean isGameOver() {
		
		// check if game is over according to game terminator
		return gameTerminator.isGameOver(currPlayerSide);
	}
	
	/**
	 * Checks if piece to be moved at current turn is killed by a Creature
	 * @param movement- movement in current turn
	 * @return True if piece to be moved is killed by a Creature, False if not
	 */
	private boolean isKilledByCreature(Movement movement) {
		
		// get Creature positions
		List<Point> creaturePositions = boardManager.getCreaturePositions();
		
		// check if there are any Creatures on the board
		if (creaturePositions.isEmpty()) {
			
			// no Creatures
			return false;
		}
		
		// check if piece is Creature-proof
		List<Point> path = null;
		Piece pieceToMove = boardManager.getPiece(movement.getSource().x, movement.getSource().y);
		
		if (pieceToMove instanceof King) {
			
			// get path
			path = getPath(movement.getSource(), movement.getDestination());
			
			// kill Creatures on King's path
			killCreaturesOnPath(path, movement.getDestination());
			
		} else if (pieceToMove instanceof Knight) {
			
			// get path
			path = getPathKnight(movement.getSource(), movement.getDestination());
			
			// kill Creatures on Knight's path
			killCreaturesOnPath(path, movement.getDestination());
			
		} else {
			
			// get path
			path = getPath(movement.getSource(), movement.getDestination());
			
			// check if passes through Creatures			 
			for (Point currPositionOnPath : path) { 
				 
				for (Point currCreature : creaturePositions) {
					 
					if (currCreature.equals(currPositionOnPath)) {
						 
						// get position of Creature that destroys the piece
						killerCreature = new Point(currCreature);
						
						// indicate that removing current piece
						return true;
					}
				}
			}
		}
		
		// piece not killed
		return false;		
	}
	
	/**
	 * Scans given path and kills all Creatures that lay on the path
	 * @param path- positions that lay between movement's source and destination position
	 * @param destination- movement's destination
	 */
	private void killCreaturesOnPath(List<Point> path, Point destination) {
		
		// get Creature positions
		List<Point> creaturePositions = boardManager.getCreaturePositions();
		
		// remove all Creatures that lay on given path
		for (Point currPosition : path) {
			if (creaturePositions.contains(currPosition)) {
				
				// remove Creature
				creaturePositions.remove(currPosition);
				
				// update killed Creatures
				killedCreatures.add(currPosition);
			}
		}
		
		// ignore movement's destination
		killedCreatures.remove(destination);
	}
	
	/**
	 * Gets two positions and returns all positions that lay on the path between them 
	 * @param srcPosition- path's source position
	 * @param destPosition- path's destination position
	 * @return all points that lay on the path
	 */
	private List<Point> getPath(Point srcPosition, Point destPosition) {
		 
		// allocate list to keep all positions on path
		List<Point> path = new LinkedList<Point>();
		 
		// find difference in row and column
		int rowDiff = destPosition.x - srcPosition.x;
		int colDiff = destPosition.y - srcPosition.y;
		 
		// find all positions on path
		Point currPosition = new Point(srcPosition);
		 		 		 
		// initialize step size on each axis
		int rowStep = 0;
		int colStep = 0;
		 
		// get row difference sign
		if (rowDiff != 0) {
			rowStep = rowDiff / Math.abs(rowDiff);			 
		}
		 
		// get column difference sign
		if (colDiff != 0) {			 
			colStep = colDiff / Math.abs(colDiff);
		}
		 
		// scan path from source to destination
		while (!currPosition.equals(destPosition)) {
			 
			// check if can move on row
			if (currPosition.x != destPosition.x) {
				
				// move on row
				currPosition.translate(rowStep, 0);
			}
				 
			// check if can move on column
			if (currPosition.y != destPosition.y) {
				
				// move on column
				currPosition.translate(0, colStep);
			}
			 
			// add current position on path
			path.add(new Point(currPosition));
		}
		 
		// found all positions on path
		return path;
	}
	
	/**
	 * Gets two positions and returns all positions that lay on the path between them.
	 * Calculates the path according to Knight's move 
	 * @param srcPosition- path's source position
	 * @param destPosition- path's destination position
	 * @return all points that lay on the path
	 */
	private List<Point> getPathKnight(Point srcPosition, Point destPosition) {
		 
		// allocate list to keep all positions on path
		List<Point> path = new LinkedList<Point>();
		 
		// find difference in row and column
		int rowDiff = destPosition.x - srcPosition.x;
		int colDiff = destPosition.y - srcPosition.y;
		
		// check if should move 2 steps on row axis
		if (Math.abs(rowDiff) == 2) {

			// calculate step sign
			int rowStep = rowDiff / Math.abs(rowDiff);
			 
			// add positions on path
			path.add(new Point(srcPosition.x + rowStep, srcPosition.y));
			path.add(new Point(srcPosition.x + 2*rowStep, srcPosition.y));
			path.add(new Point(destPosition));
			 
		} else { // move 2 steps on column axis

			// calculate step sign
			int colStep = colDiff / Math.abs(colDiff);
			 
			// add positions on path
			path.add(new Point(srcPosition.x, srcPosition.y + colStep));
			path.add(new Point(srcPosition.x, srcPosition.y + 2*colStep));
			path.add(new Point(destPosition));
		}
		 
		// found all positions on path 
		return path;
	 }
	
	/**
	 * Ends currently running game
	 */
	@Override
	public void endGame() {
		// set keep playing indicator
		keepPlaying = false;
	}
	
	/**
	 * Adds piece of given type at given position assigned to given player.
	 * Returns True if could add the piece, False if not
	 */
	@Override
	public boolean addPiece(Point position, String type, PlayerSides.Sides pieceSide) {

		// add piece with given details to the board
		return boardManager.addPiece(position, type, pieceSide); 
	}
	
	/**
	 * Returns list of possible moves of piece at given position.
	 * If no piece at given position or if piece at given position is not assigned to currently playing
	 * player, an empty list is returned.
	 * Otherwise, gets move list of the piece and returns only the moves that do not result in friendly
	 * King being threatened
	 */
	@Override
	public List<Point> getSafeDestinations(Point srcPosition) {
		
		// initialize move list
		List<Point> moveList = new LinkedList<Point>();
		
		// check if there is a piece at given position
		if (boardManager.getPiece(srcPosition.x, srcPosition.y) == null) {
			
			// no piece at given position
			return moveList;
		}
		
		// check if piece at given position is assigned to current player
		if (!boardManager.getPiece(srcPosition.x, srcPosition.y).getPlayer().equals(currPlayerSide)) {
			
			// piece at given position cannot move at current turn
			return moveList;
		}
		
		// piece can move at current turn, get its moves
		moveList.addAll(boardManager.getPiece(srcPosition.x, srcPosition.y).getAvailableDestinations(boardManager.getBoard(), srcPosition));
		
		// remove moves that result in check
		Iterator<Point> iter = moveList.iterator();
		while (iter.hasNext()) {
			
			// get current move
			Point currMove = new Point(iter.next());
			
			// check if current move is safe
			if (threatAnalyzer.moveNotSafe(srcPosition, currMove, currPlayerSide)) {
				
				// current move is not safe, remove from list
				iter.remove();
			}
		}
		
		// return all safe moves left
		return moveList;
	}
	
	/**
	 * Uses board manager to place Creatures on board
	 */
	@Override
	public void placeCreatures() {
		
		// place Creatures on board
		boardManager.placeCreatures();
	}
	
	/**
	 * Returns maximal number of pieces for each player in partial game
	 */
	@Override
	public int getMaxPiecesPertialGame() { return MAX_PIECES_PARTIAL_GAME; }
}